#pragma once

#include	<condition_variable>
#include	<functional>
#include	<map>
#include	<memory>
#include	<mutex>
#include	<queue>
#include	<thread>
#include	<vector>
#include	<google/protobuf/message.h>

struct st_mysql;
struct st_mysql_field;
struct st_mysql_res;

namespace SQL {
	using namespace std;

	/**
	 * Shortcut for shared cursor.
	 */
	typedef shared_ptr<class Cursor> SharedCursor;

	/**
	 * MySQL connection.
	 */
	class Database {
	public:
		Database() : _valid(false), _ctx(nullptr) {}
		virtual ~Database() { Close(); }

		/**
		 * Connect to MySQL server & open given database.
		 *
		 * \param	host	Host address of MySQL server.
		 * \param	port	MySQL open port. Default is 3306.
		 * \param	user	User name to login on MySQL.
		 * \param	pswd	Password used to login.
		 * \param	db		Database to be used.
		 * \return Whether connected to MySQL successfully or not?
		 */
		bool Open(const string & host, int port, const string & user, const string & pswd, const string & db);

		/**
		 * Disconnect with MySQL server.
		 */
		void Close();

		/**
		 * Execute SQL for INSERT/DELETE/UPDATE those need no query data to be return.
		 *
		 * \param	sql	SQL statement to be executed.
		 * \return Number of rows effected by this operation.
		 */
		int Exec(const string & sql);

		/**
		 * Query MySQL.
		 *
		 * \param	sql	SQL statement.
		 * \return Cursor for traversing result data.
		 */
		SharedCursor Query(const string & sql);

		/**
		 * MySQL escape string.
		 *
		 * \param	source	Source string.
		 * \return MySQL safe string.
		 */
		string Escape(const string & source);

		/**
		 * Get id of last inserted row.
		 */
		int64_t LastInsertId();

	public:
		/**
		 * Insert google::protobuf::Message data into given table.
		 *
		 * \param	table	Name of table to be inserted to.
		 * \param	data	Message data.
		 * \return Operation result.
		 */
		int Insert(const string & table, const google::protobuf::Message & data);

		/**
		 * Update data in table by given key. Data is stored in google::protobuf::Message.
		 *
		 * \param	table	Name of table to be updated.
		 * \param	data	Data content.
		 * \param	key		Key to find data to be updated. NOTE : key MUST be a property of given data.
		 * \return Operation result.
		 */
		int Update(const string & table, const google::protobuf::Message & data, const string & key);

	private:
		string __Serialize(
			const google::protobuf::Message & source,
			const google::protobuf::Reflection * reflect,
			const google::protobuf::FieldDescriptor * field);

	private:
		bool _valid;
		st_mysql * _ctx;
	};

	/**
	 * Cursor for traverse query result.
	 */
	class Cursor {
	public:
		Cursor(st_mysql_res * res);
		virtual ~Cursor();

		/**
		 * Move cursor to next. NOTE : you should call this once before real 
		 *
		 * \return True for move successfully.
		 */
		bool Next();

		/**
		 * Check if data in given column of current row is NULL.
		 *
		 * \param	column	Column name for data stored in current row.
		 * \return True for data is SQL null.
		 */
		bool IsNull(const string & column);

		/**
		 * Get data in given column of current row.
		 *
		 * \typeparam	T		Data type. Support : bool, [u]int8_t, [u]int16_t, [u]int32_t, [u]int64_t, float, double, string(for simple string & blob)
		 * \param		column	Column name for data stored in current row.
		 * \return Data.
		 */
		template<typename T>
		T Get(const string & column);

		/**
		 * Get date time.
		 *
		 * \param	column	Column name for data stored in current row.
		 * \return Unix timestamp.
		 */
		time_t GetTimestamp(const string & column);

		/**
		 * Cast current row into google::protobuf::Message type.
		 *
		 * \typeparam	T	Derived class of google::protobuf::Message.
		 * \return Parsed message.
		 */
		template<class T>
		T Cast() { T msg; __Cast(msg); return move(msg); }

	private:
		int __Check4Get(const string & column);
		void __Cast(google::protobuf::Message & msg);

	private:
		st_mysql_res * _res;
		char ** _current;
		unsigned long * _len;
		map<string, int> _key2idx;
		vector<st_mysql_field *> _fields;
	};

	/**
	 * Interface of tasks for using connection pool.
	 */
	struct ITask {
		virtual ~ITask() {}
		virtual void OnRun(SQL::Database & db) = 0;
	};

	/**
	 * Connection pool.
	 */
	class Pool {
	public:
		Pool(int workers);
		virtual ~Pool();

		/**
		 * Initialize connection pool
		 *
		 * \param	host	Host of mysql.
		 * \param	port	Port of mysql.
		 * \param	user	User to login.
		 * \param	pswd	Password for user.
		 * \param	db		Using database.
		 * \return Initialize result.
		 */
		bool Open(const string & host, int port, const string & user, const string & pswd, const string & db);

		/**
		 * Push job in queue.
		 *
		 * \param	args...	Parameter to create job T.
		 * \return Whether job has been pushed in queue successfully or not.
		 */
		template<class T, typename ... Args>
		bool Push(Args && ... args) {
			if (!_valid) return false;

			T * task = new T(args...);
			{
				unique_lock<mutex> _(_lock);
				_jobs.push(task);
			}
			_signal.notify_one();

			return true;
		}

	private:
		void __Work(Database * db);

	private:
		bool _valid;
		int _size;
		queue<ITask *> _jobs;
		queue<thread *> _workers;
		mutex _lock;
		condition_variable _signal;
	};
}